#include <cassert>
#include <chrono>
#include <stdarg.h>
#include "DBPool.hpp"


#define DEBUG(format, ...)	



const auto& ERROR_INDEX_OUT_OF_RANGE = std::out_of_range("Index out of range");

int db_result_callback(void *ResultSet, int argc, char **argv, char **azColName)
{
	DBResultSet* set = static_cast<DBResultSet*>(ResultSet);
	DBResultSet::ResultMap *map = new DBResultSet::ResultMap();
	for (int i=0; i < argc; i++) {
		map->insert(std::pair<std::string, std::string>(azColName[i], argv[i] ? argv[i] : "NULL"));
	}
	set->addResultMap_(map);
	return 0;
}


DBResultSet::~DBResultSet()
{
	for (auto it : resVec_) {
		if (it == nullptr) {
			continue;
		}
		delete it;
	}
}


size_t	DBResultSet::getColNum(int index) const
{
	if (index >= getResultNum()) {
		throw std::out_of_range("Index out of range");
	}
	return resVec_[index]->size();
}


int DBResultSet::getInt(const string& key, int index) noexcept(false)
{
	const string &value = findValue_(key, index);
	if (value == "NULL") {
		return 0;
	}
	return std::stoi(value);
}


float DBResultSet::getFloat(const string& key, int index) noexcept(false)
{
	const string &value = findValue_(key, index);
	if (value == "NULL") {
		return 0;
	}
	return std::stof(value);
}


const DBResultSet::string& DBResultSet::getString(const string& key, int index) noexcept(false)
{
	return findValue_(key, index);
}


const DBResultSet::string& DBResultSet::findValue_(const string &key, int index) const
{
	if (index >= getResultNum()) {
		throw std::out_of_range("Index out of range");
	}
	const ResultMap *map = resVec_[index];
	const ResultMap::const_iterator& it = map->find(key);
	if (it == map->end()) {
		throw std::out_of_range("Key not found");
	}
	return it->second;
}


/* ------------------------------------ */


DBPrepareStatement::~DBPrepareStatement()
{
	if (stmt_) {
		sqlite3_finalize(stmt_);
	}
}



bool DBPrepareStatement::init(sqlite3 *db, std::string &sql)
{
	if (db == nullptr) { return false; }

	int ret = sqlite3_prepare_v2(db, sql.c_str(), sql.length(), &stmt_, nullptr);
	if (ret != SQLITE_OK) {
		DEBUG("error: %s", sqlite3_errmsg(db));
		return false;
	}

	return true;
}


bool DBPrepareStatement::setParam(int index, int param)
{
	if (index >= param_cnt_) {
		throw ERROR_INDEX_OUT_OF_RANGE;
	}

	int ret = sqlite3_bind_int(stmt_, index, param);
	if (ret == SQLITE_OK) {
		return true;
	} 
		
	DEBUG("error: %s", sqlite3_errmsg(db));
	return false;
}



bool DBPrepareStatement::setParam(int index, const std::string& text)
{
	if (index >= param_cnt_) {
		throw ERROR_INDEX_OUT_OF_RANGE;
	}

	int ret = sqlite3_bind_text(stmt_, index, text.c_str(), text.length(), nullptr);
	if (ret != SQLITE_OK) {
		DEBUG("error: %s", sqlite3_errstr(ret));
		return false;
	}
		
	return true;
}



auto DBPrepareStatement::execute()
{
	// TODO
	int ret;
	ret = sqlite3_step(stmt_);
	if (ret == SQLITE_ROW) {

	} else if (ret == SQLITE_DONE) {

	} else {

	}
}



bool DBPrepareStatement::reset()
{
	int ret;
	ret = sqlite3_reset(stmt_);
	if (ret != SQLITE_OK) {
		DEBUG("error: %s", sqlite3_errstr(ret));
		return false;
	}

	return true;
}




/* ------- */

DBConn::~DBConn()
{
	if (db_) {
		sqlite3_close(db_);
		db_ = nullptr;
	}
}



int DBConn::init()
{
	int ret;
	ret = sqlite3_open(db_path_.c_str(), &db_);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db_));
	}
	assert(db_);
	
	return 0;
}


bool DBConn::startTransaction()
{
	int ret;
	char *zErrMsg = 0;
	
	const std::string sql = "start transaction;";
	ret = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &zErrMsg);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "SQL error: %s\n", zErrMsg);
		sqlite3_free(zErrMsg);
		return false;
	}
	
	fprintf(stdout, "Connection is created successfully\n");

	return true;
}


bool DBConn::commit()
{
	int ret;
	char *zErrMsg = 0;
	
	const std::string sql = "commit;";
	ret = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &zErrMsg);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "SQL error: %s\n", zErrMsg);
		sqlite3_free(zErrMsg);
		return false;
	}

	return true;
}


bool DBConn::rollback()
{
	int ret;
	char *zErrMsg = 0;
	
	const std::string sql = "rollback;";
	ret = sqlite3_exec(db_, sql.c_str(), nullptr, nullptr, &zErrMsg);
	if (ret != SQLITE_OK) {
		fprintf(stderr, "SQL error: %s\n", zErrMsg);
		sqlite3_free(zErrMsg);
		return false;
	}

	return true;
}


DBResultSetPtr DBConn::executeQuery(const std::string &sql)
{
	int ret;
	char *zErrMsg = nullptr;
	DBResultSetPtr pSet = std::make_shared<DBResultSet>();
	if (!pSet) {
        printf("ptr is not a valid pointer.\n");
    }

	ret = sqlite3_exec(db_, sql.c_str(), db_result_callback, pSet.get(), &zErrMsg);
	if (ret != SQLITE_OK) {
		fprintf(stdout, "SQL error: %s\n", zErrMsg);
		sqlite3_free(zErrMsg);
		return pSet;
	}

	return pSet;
}


bool DBConn::executeUpdate(const std::string &sql) 
{

	return true;	
}



/*    --------    */


DBPool::~DBPool()
{
	std::lock_guard<std::mutex> lock(mutex_);

	pool_abort_ = true;

	cond_var_.notify_all();

	ListT::const_iterator cit = free_list_.cbegin();
	for (; cit != free_list_.cend(); cit++) {
		DBConn *pConn = *cit;
		delete pConn;
	}

	free_list_.clear();
}


int DBPool::init()
{
	for (int i = 0; i < db_cur_conn_cnt_; i++) {
		DBConn *pConn = new DBConn(getPoolName(), getDBPath());
		assert(pConn);
		int ret = pConn->init();
		if (ret) {
			delete pConn;
			return ret;
		}
		free_list_.emplace_back(pConn);
	}
	return 0;
}


DBConn* DBPool::getDBConn(const uint32_t timeout)
{
	std::unique_lock<std::mutex> lock(mutex_);
	if (pool_abort_) {
		fprintf(stderr, "Pool abort \n");
		return nullptr;
	}

	if (free_list_.empty()) {
		if (db_cur_conn_cnt_ <= db_max_conn_cnt_) {
			DBConn *pConn = new DBConn(getPoolName(), getDBPath());
			assert(pConn);
			int ret = pConn->init();
			if (ret) {
				delete pConn;
				fprintf(stderr, "Init DBConnecton failed");
				return nullptr;
			}
			free_list_.push_back(pConn);
			db_cur_conn_cnt_++;
		} else {
			if (timeout <= 0) {
				cond_var_.wait(lock, [this](){ return (!free_list_.empty()) || (pool_abort_); });
			} else {
				cond_var_.wait_for(lock, std::chrono::milliseconds(timeout), [this]{
					return (!free_list_.empty()) || (pool_abort_);
				});

				if (free_list_.empty()) {
					fprintf(stderr, "timeout");
					return nullptr;
				}
			}

			if (pool_abort_) {
				fprintf(stderr, "Pool abort \n");
				return nullptr;
			}
		}
	}

	DBConn *pConn = free_list_.front();
	assert(pConn);
	free_list_.pop_front();
	return pConn;
}



void DBPool::relDBConn(DBConn *pConn)
{
	if (pConn == nullptr) { return; }

	std::lock_guard<std::mutex> lock(mutex_);

	ListT::const_iterator cit = free_list_.cbegin();
	for (; cit != free_list_.cend(); cit++) {
		if (*cit == pConn) {
			break;
		}
	}

	if (cit == free_list_.cend()) {
		free_list_.push_back(pConn);
		cond_var_.notify_one();
	}

}




/* ---------- */


DBManager::DBManager()
{
	std::string pool_name;
	std::string db_name;
	std::string db_path;
	uint8_t		db_max_conn_cnt;

	const auto &config = gDBConfig;
	for (int i = 0; i < config.size(); i++) {
		pool_name = config[i].find("pool_name")->second;
		db_path = config[i].find("db_path")->second;
		db_max_conn_cnt = std::stoi(config[i].find("db_max_conn_cnt")->second);

		DBPool *pool = new DBPool(pool_name.c_str(), db_name.c_str(), db_path.c_str(), db_max_conn_cnt);
		assert(pool);
		pool_map_.insert({pool_name, pool});
	}
	printf("DBManager is created\n");
}

DBManager::~DBManager()
{
	auto it = pool_map_.begin();
	for (; it != pool_map_.end(); it++) {
		delete it->second;
		it->second = nullptr;
	}
}




DBConn* DBManager::getDBConn(const std::string &pool_name, uint32_t timeout)
{
	const auto& it = pool_map_.find(pool_name);
	if (it == pool_map_.end()) {
		fprintf(stdout, "Pool name is not found %s", pool_name.c_str());
		return nullptr;
	}
	return it->second->getDBConn(timeout);
}



void DBManager::relDBConn(DBConn * pConn)
{
	if (pConn == nullptr) { return; }

	const std::string &pool_name = pConn->getPoolName();
	const auto &it = pool_map_.find(pool_name);
	if (it == pool_map_.end()) {
		return;
	}

	it->second->relDBConn(pConn);
}


DBManager& DBManager::getInstance()
{
	static DBManager *instance = new DBManager();
	assert(instance);
	return *instance;
}


